
"""
To use the code, either execute it in SageMath or launch Sage kernel under the same directoy of this file and do:

sage: from Vn_sql import *

Be sure to edit sql_path and schema before you do the above. 

The following methods are provided in this file:

    knot_info(n, snappy_name): 
        given an integer n and a string representing the name of a knot,
        provided that the Vn polynomial of the given knot is stored in the databases invoked by schema,
        return the dictionary of information of that knot stored in the databases.
        For example:

        sage: knot_info(1,'4_1')
        >> {'KnotTheory Name': 'Knot[4, 1]',
            'PD Code': '[(1, 6, 2, 7), (5, 2, 6, 3), (3, 1, 4, 8), (7, 5, 8, 4)]',
            'Genus': 1,
            'Degree of Alexander Polynomial': 2,
            'Tightness': 1,
            'V1 Polynomial': '7 + 2/q^2 + 2*q^2 + t^(-2) - 3/(q*t) - (3*q)/t - (3*t)/q - 3*q*t + t^2'}

    knot_table(n, cross_num, sort = True):
        given two integers n and cross_num,
        return the list of all knots with cross_num corssings whose Vn polynomial is stored in the databases.
        For example:

        sage: knot_table(1,6)
        >> ['6_1', '6_2', '6_3']

        The variable sort controls whether the returned list of knots is sorted. (Setting it to False may save time in some scenarios)

    genus(n, snappy_name)
        given an integer n and a string representing the name of a knot,
        provided that the Vn polynomial of the given knot is stored in the databases invoked by schema,
        return the integer representing the genus of the given knot

    alex_degree(n, snappy_name)
        given an integer n and a string representing the name of a knot,
        provided that the Vn polynomial of the given knot is stored in the databases invoked by schema,
        return the integer representing the degree of the alexander polynomial of the given knot
        (We take the negative powers into account when defining the degree of laurent polynomials)

    is_tight(n, snappy_name)
        given an integer n and a string representing the name of a knot,
        provided that the Vn polynomial of the given knot is stored in the databases invoked by schema,
        return the integer representing the tightness of the given knot (1 for tight, 0 for not tight)

    PD(n, snappy_name)
        given an integer n and a string representing the name of a knot,
        provided that the Vn polynomial of the given knot is stored in the databases invoked by schema,
        return the list representing the planar diagram of the given knot in SnapPy convention.

    V(n, snappy_name)
        given an integer n and a string representing the name of a knot,
        provided that the Vn polynomial of the given knot is stored in the databases invoked by schema,
        return the LaurentPolynomial instant in Sage representing the Vn polynomial fo the given knot
        (see https://doc.sagemath.org/html/en/reference/polynomial_rings/sage/rings/polynomial/laurent_polynomial.html)
    
    is_nonalt(snappy_name)
        given a string representing the name of a knot,
        return True if the knot is nonalternating, False if otherwise.
        (this method is in fact independent of this file)
"""


# This file was *autogenerated* from the file Vn_sql.sage
from sage.all_cmdline import *   # import sage library

_sage_const_1 = Integer(1); _sage_const_3 = Integer(3); _sage_const_5 = Integer(5); _sage_const_0 = Integer(0); _sage_const_8 = Integer(8); _sage_const_19 = Integer(19); _sage_const_9 = Integer(9); _sage_const_42 = Integer(42); _sage_const_10 = Integer(10); _sage_const_124 = Integer(124)
import sqlite3
import re, ast

# Edit path accordingly, pointing to the folder containing all SQLite database files needed
sql_path = "/Users/"

# Names of databases and tables in them you want to access.
# Files should all be stored under the directory sql_path points to
schema = {
    'V-database_3-16c.db' : ['V1', 'V2', 'V3', 'V4'],
    'V-database_17c-loose.db' : ['V1', 'V2'],
    'V-database_18c-loose.db' : ['V1', 'V2'],
    'V1-database_17a.db' : ['V1'],
    'V1-database_18a.db' : ['V1']
}

# The rest is automatic and you have nothing to worry about

assert schema

table_dict = dict()
alias_dict = dict()

V_conn = sqlite3.connect(':memory:')
cursor = V_conn.cursor()

for i, sql_name in enumerate(schema.keys(), start = _sage_const_1 ):
    alias = f'db{i}'

    alias_dict.update({sql_name : alias})

    for table_name in schema[sql_name]:
        if table_name not in table_dict.keys():
            table_dict.update({table_name : [alias]})
        else:
            table_dict.update({table_name : table_dict[table_name] + [alias]})

    cursor.execute('ATTACH DATABASE ? AS ?', (sql_path + sql_name, alias))


for table_name in table_dict.keys():
    select_statements = []
    for alias in table_dict[table_name]:
        select_statements.append(f"SELECT * FROM {alias}.{table_name}")
    
    union_query = ' UNION ALL '.join(select_statements)

    view_name = f'all_{table_name}'

    create_view_sql = f"CREATE TEMPORARY VIEW {view_name} AS {union_query}"

    cursor.execute(create_view_sql)

cursor.close()

def laurent_poly_from_str(string, vars = ['t','q']):
    F = PolynomialRing(ZZ, vars).fraction_field()
    L = LaurentPolynomialRing(ZZ, vars)

    return L(F(string))

def laurent_poly_from_dict(dict, vars):
    L = LaurentPolynomialRing(ZZ, vars)
    return L(dict)

def degree(laurent_poly, vars, var):
    L = LaurentPolynomialRing(ZZ, vars)
    t = L.gens()[vars.index(var)]

    return laurent_poly.degree(t) - laurent_poly.valuation(t)

def t_degree(v_poly):
    return degree(v_poly, ['t','q'], 't')

def q_degree(v_poly):
    return degree(v_poly, ['t','q'], 'q')

def knot_info(n, snappy_name, conn = V_conn):
    """
    Selects knot data from a specific table based on its SnapPy Name.
    
    Args:
        conn: An active sqlite3 connection object.
        n (int): The integer used to specify the table (Vn) and polynomial column.
        snappy_name (str): The SnapPy name of the knot to find.

    Returns:
        list: A list of tuples, where each tuple represents a row from the database.
              Returns an empty list if no match is found.
    """
    # Create a cursor object to execute SQL commands
    cursor = conn.cursor()

    table_name = f"V{n}"
    poly_column_name = f"V{n} Polynomial"

    header = ['KnotTheory Name', 'PD Code', 'Genus', 'Degree of Alexander Polynomial', 'Tightness', poly_column_name]

    query = f"""
        SELECT
            "KnotTheory Name",
            "PD Code",
            Genus,
            "Degree of Alexander Polynomial",
            Tightness,
            "{poly_column_name}"
        FROM
            "all_{table_name}"
        WHERE
            "SnapPy Name" = ?
    """

    try:
        cursor.execute(query, (snappy_name,))
        
        results = cursor.fetchall()
        return dict(zip(header, results[_sage_const_0 ]))
    except sqlite3.Error as e:
        print(f"An error occurred with sqlite3: {e}")
        return dict()
    except IndexError as e:
        print(f"Knot name unknown")
        return dict()
    
def knot_table(n, cross_num, sort = True, conn = V_conn):
    cursor = conn.cursor()
    table_name = f"V{n}"

    query = f'''
        SELECT "SnapPy Name"
        FROM "all_{table_name}"
        WHERE CAST(
            SUBSTR("SnapPy Name", 1, LENGTH("SnapPy Name") - LENGTH(LTRIM("SnapPy Name", '0123456789')))
            AS INTEGER) = ?
    '''
    params = [int(cross_num)]
    
    try:
        cursor.execute(query, params)
        filtered_knots = [row[_sage_const_0 ] for row in cursor.fetchall()]
    except sqlite3.Error as e:
        print(f"An error occurred: {e}")
        return []

    if sort:
        key_func = lambda name: [re.sub(r'[0-9]', '', name)] + [int(part) for part in re.split(r'[a-zA-Z]*_|[an]', name)]
        ans = sorted(filtered_knots, key=key_func)
    else:
        ans = filtered_knots
    
    return ans

def genus(n,snappy_name, conn = V_conn):
    return int(knot_info(n , snappy_name, conn)['Genus'])
def alex_degree(n, snappy_name, conn = V_conn):
    return int(knot_info(n , snappy_name, conn)['Degree of Alexander Polynomial'])
    
def is_tight(n, snappy_name, conn = V_conn):
    return int(knot_info(n , snappy_name, conn)['Tightness'])

def PD(n, snappy_name, conn = V_conn):
    return ast.literal_eval(knot_info(n, snappy_name, conn)['PD Code'])

def V(n, snappy_name, conn = V_conn):
    return laurent_poly_from_str(knot_info(n, snappy_name, conn)[f"V{n} Polynomial"])

def is_nonalt(snappy_name):
    if len(snappy_name.split('n')) > _sage_const_1 :
        return True
    elif len(snappy_name.split('a')) > _sage_const_1 :
        return False
    else:
        [cross, i] = snappy_name.split('_')
        cross = int(cross)
        i = int(i)
        if (cross == _sage_const_8  and i >= _sage_const_19 ) or (cross == _sage_const_9  and i >= _sage_const_42 ) or (cross == _sage_const_10  and i >= _sage_const_124 ):
            return True
        else:
            return False

